/* $Id$ */
/* ----------------------------------------------------------------------- *
 *
 *   Copyright 2001 H. Peter Anvin - All Rights Reserved
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA 02139,
 *   USA; either version 2 of the License, or (at your option) any later
 *   version; incorporated herein by reference.
 *
 * ----------------------------------------------------------------------- */

/*
 * mkzffile.c
 *
 *	- Generate block-compression of files for use with
 *	  the "ZF" extension to the iso9660/RockRidge filesystem.
 *
 *	  The file compression technique used is the "deflate"
 *	  algorithm used by the zlib library; each block must have a
 *	  valid (12-byte) zlib header.  In addition, the file itself
 *	  has the following structure:
 *
 *	  Byte offset	iso9660 type	Contents
 *	    0		(8 bytes)	Magic number (37 E4 53 96 C9 DB D6 07)
 *	    8		7.3.1		Uncompressed file size
 *	   12		7.1.1		header_size >> 2 (currently 4)
 *	   13		7.1.1		log2(block_size)
 *	   14		(2 bytes)	Reserved, must be zero
 *
 * The header may get expanded in the future, at which point the
 * header size field will be used to increase the space for the
 * header.
 *
 * All implementations are required to support a block_size of 32K
 * (byte 13 == 15).
 *
 * Note that bytes 12 and 13 and the uncompressed length are also
 * present in the ZF record; THE TWO MUST BOTH BE CONSISTENT AND
 * CORRECT.
 *
 * Given the uncompressed size, block_size, and header_size:
 *
 *     Nblocks := ceil(size/block_size)
 *
 * After the header follow (nblock+1) 32-bit pointers, recorded as
 * iso9660 7.3.1 (littleendian); each indicate the byte offset (from
 * the start of the file) to one block and the first byte beyond the
 * end of the previous block; the first pointer thus point to the
 * start of the data area and the last pointer to the first byte
 * beyond it:
 *
 *     block_no := floor(byte_offset/block_size)
 *
 *     block_start := read_pointer_731( (header_size+block_no)*4 )
 *     block_end   := read_pointer_731( (header_size+block_no+1)*4 )
 *
 * The block data is compressed according to "zlib".
 */

#include "mkzftree.h" /* Must be included first! */

#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>

#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif

#include "version.h"

/* Command line options */
struct cmdline_options opt = {
    0,                  /* Force compression */
    0,                  /* Compression algorithm */
    9,                  /* Compression level */
    0,                  /* Parallelism (0 = strictly serial) */
    0,                  /* One filesystem only */
    0,                  /* One directory only */
    1,                  /* Create stub directories */
    0,                  /* Root may be a file */
    0,                  /* Be paranoid about metadata */
    default_verbosity,  /* Default verbosity */
    block_compress_file /* Default transformation function */
};

/* Program name */
const char *program;

/* Long options */
#define OPTSTRING "fa:z:up:xXC:lLFvqV:hws"
#ifdef HAVE_GETOPT_LONG
const struct option long_options[] = {
    {"force", 0, 0, 'f'},
    {"algorithm", 0, 0, 'a'},
    {"level", 1, 0, 'z'},
    {"uncompress", 0, 0, 'u'},
    {"parallelism", 1, 0, 'p'},
    {"one-filesystem", 0, 0, 'x'},
    {"strict-one-filesystem", 0, 0, 'X'},
    {"crib-tree", 1, 0, 'C'},
    {"local", 0, 0, 'l'},
    {"strict-local", 0, 0, 'L'},
    {"file", 0, 0, 'F'},
    {"verbose", 0, 0, 'v'},
    {"quiet", 0, 0, 'q'},
    {"verbosity", 1, 0, 'V'},
    {"help", 0, 0, 'h'},
    {"version", 0, 0, 'w'},
    {"sloppy", 0, 0, 's'},
    {0, 0, 0, 0}};
#define LO(X) X
#else
#define getopt_long(C, V, O, L, I) getopt(C, V, O)
#define LO(X)
#endif

static void usage(enum verbosity level, int err)
{
  message(level,
          "zisofs-tools " ZISOFS_TOOLS_VERSION "\n"
          "Usage: %s [options] intree outtree\n"
          "  --force                  -f    Always compress, even if result is larger\n"
          "  --algorithm #            -a #  Set compression algorithm: 0* (zlib)\n"
          "  --level #                -z #  Set compression level (1-9)\n"
          "  --uncompress             -u    Uncompress an already compressed tree\n"
          "  --parallelism #          -p #  Process up to # files in parallel\n"
          "  --one-filesystem         -x    Do not cross filesystem boundaries\n"
          "  --strict-one-filesystem  -X    Same as -x, but don't create stubs dirs\n"
          "  --crib-tree              -C    Steal \"crib\" files from an old tree\n"
          "  --local                  -l    Do not recurse into subdirectoires\n"
          "  --strict-local           -L    Same as -l, but don't create stubs dirs\n"
          "  --file                   -F    Operate possibly on a single file\n"
          "  --sloppy                 -s    Don't abort if metadata cannot be set\n"
          "  --verbose                -v    Increase message verbosity\n"
          "  --verbosity #            -V #  Set message verbosity to # (default = %d)\n"
          "  --quiet                  -q    No messages, not even errors (-V 0)\n"
          "  --help                   -h    Display this message\n"
          "  --version                -w    Display the program version\n",
          program, (int)default_verbosity);
  exit(err);
}

static int opt_atoi(const char *str)
{
  char *endptr;
  long out;

  out = strtol(str, &endptr, 10);
  if (*endptr)
    usage(vl_error, EX_USAGE);

  return (int)out;
}

int main(int argc, char *argv[])
{
  const char *in, *out, *crib = NULL;
  struct stat st;
  int optch, err;

  program = argv[0];

  while ((optch = getopt_long(argc, argv, OPTSTRING, long_options, NULL)) != EOF)
  {
    switch (optch)
    {
    case 'f':
      opt.force = 1; /* Always compress */
      break;
    case 'a':
      opt.algorithm = opt_atoi(optarg);
      if (opt.algorithm != ZISOFS_ZLIB && opt.algorithm != ZISOFS_LZMA)
      {
        message(vl_error, "%s: invalid compression algorithm: %d\n",
                program, opt.algorithm);
        exit(EX_USAGE);
      }
      break;
    case 'z':
      opt.level = opt_atoi(optarg);
      if (opt.level < 1 || opt.level > 9)
      {
        message(vl_error, "%s: invalid compression level: %d\n",
                program, optarg);
        exit(EX_USAGE);
      }
      break;
    case 'v':
      opt.verbosity++;
      break;
    case 'V':
      opt.verbosity = opt_atoi(optarg);
      break;
    case 'q':
      opt.verbosity = vl_quiet;
      break;
    case 'u':
      opt.munger = block_uncompress_file;
      break;
    case 'C':
      crib = optarg;
      break;
    case 'p':
      opt.parallel = opt_atoi(optarg);
      break;
    case 'x':
      opt.onefs = 1;
      opt.do_mkdir = 1;
      break;
    case 'l':
      opt.onedir = 1;
      opt.do_mkdir = 1;
      break;
    case 'X':
      opt.onefs = 1;
      opt.do_mkdir = 0;
      break;
    case 'L':
      opt.onedir = 1;
      opt.do_mkdir = 0;
      break;
    case 'F':
      opt.file_root = 1;
      break;
    case 's':
      opt.sloppy = 1;
      break;
    case 'h':
      usage(vl_quiet, 0);
      break;
    case 'w':
      message(vl_quiet, "zisofs-tools " ZISOFS_TOOLS_VERSION "\n");
      exit(0);
    default:
      usage(vl_error, EX_USAGE);
      break;
    }
  }

  if ((argc - optind) != 2)
    usage(vl_error, EX_USAGE);

  in = argv[optind];      /* Input tree */
  out = argv[optind + 1]; /* Output tree */

  umask(077);

  if (opt.file_root)
  {
    if (lstat(in, &st))
    {
      message(vl_error, "%s: %s: %s\n", program, in, strerror(errno));
      exit(EX_NOINPUT);
    }

    err = munge_entry(in, out, crib, NULL);
  }
  else
  {
    /* Special case: we use stat() for the root, not lstat() */
    if (stat(in, &st))
    {
      message(vl_error, "%s: %s: %s\n", program, in, strerror(errno));
      exit(EX_NOINPUT);
    }
    if (!S_ISDIR(st.st_mode))
    {
      message(vl_error, "%s: %s: Not a directory\n", program, in);
      exit(EX_DATAERR);
    }

    err = munge_tree(in, out, crib);
  }

  wait_for_all_workers();

  if (err)
    exit(err);

  if (!opt.file_root)
  {
    if (chown(out, st.st_uid, st.st_gid) && !opt.sloppy)
    {
      message(vl_error, "%s: %s: %s", program, out, strerror(errno));
      err = EX_CANTCREAT;
    }
    if (chmod(out, st.st_mode) && !opt.sloppy && !err)
    {
      message(vl_error, "%s: %s: %s", program, out, strerror(errno));
      err = EX_CANTCREAT;
    }
    if (copytime(out, &st) && !opt.sloppy && !err)
    {
      message(vl_error, "%s: %s: %s", program, out, strerror(errno));
      err = EX_CANTCREAT;
    }
  }

  return err;
}
